package com.redisson.service.impl;

import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson2.JSON;
import com.redisson.service.RedisService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @作者: 林江
 * @创建时间: 2024/2/10
 * @功能:
 */
@Service
public class RedisServiceImpl implements RedisService {
    private final static String CATEGORY_KEY = "category";
    @Resource
    private RedisTemplate redis;
    @Autowired
    RedissonClient redisson;

    @Override
    public String listCategoryTree() {
        // 先从缓存中获取，然后从数据库中获取
        Object value = redis.opsForValue().get(CATEGORY_KEY);
        if (value == null) {
            String str = this.load();
            return str;
        }
        System.out.println("访问redis缓存。。。。。。。。。。。。。。。。。。。。。。");
        return JSON.toJSONString(value);
    }


    /**
     * load01:
     * 问题：在并发情况下,会出现多个线程访问数据库的情况,从而造成数据库访问压力
     * 解决：使用同步锁或JUC锁。
     * load02：
     * 问题：在分布式环境下，synchronized锁就不行了，因为对于JUC和synchronized锁都是
     * 对当前进程下的线程进行加锁操作。
     * 测试：8001 / 8000 的服务中出现了多次访问数据库的操作
     * 解决：引入redis的分布式锁，setNX指令
     * load03:
     * 加锁问题1：在某种极端的情况下，redis在解锁之前就失败了。那么就会造成死锁的现象
     * 解决：加锁的时候，设置一个过期时间。
     * 加锁问题2：设置过期时间之后，如果业务代码的执行逻辑比redis设置的过期时间长，那么就会出现
     * 其他线程开始执行。但是上一个线程还没有执行完。
     * 解决：将过期时间设置长一点。获取对未完成的业务进行续期操作。
     * 删锁问题1：如果超时时间太短，就会出现A线程删除后执行的B线程的锁。
     * 解决：给一个UUID值，进行逻辑判断
     * 删锁问题2：如果在A线程获取锁的值的时候，redis返回成功，但是删除的时候，
     * 网络延时，A的锁超时了，B获取到了锁。那么A在删锁的时候，就不是自己的锁了。
     * 解决：使用redis官方提供的脚本数据
     * if redis.call("get",KEYS[1]) == ARGV[1]
     * then
     * return redis.call("del",KEYS[1])
     * else
     * return 0
     * end
     * 故官方不推荐使用load03的加锁和解锁操作。推荐使用的是开源框架reddsion。
     * load:
     *
     * @return
     */
    public String load() {
        // 获取锁
        RLock lock = redisson.getLock("lock");
        try {
            lock.lock(); // 加锁
            Object value = redis.opsForValue().get(CATEGORY_KEY);
            if (value != null) {
                System.out.println("访问redis缓存。。。。。。。。。。。。。。。。。。。。。。");
                return JSON.toJSONString(value);
            }
            System.out.println("访问数据库。。。。。。。。。。。。。。。。。。。。。。");
            String menus = listTree();
            // 模拟redis保存时，网络延时。。。
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 保存缓存中
            redis.opsForValue().set(CATEGORY_KEY, menus);
            return menus;
        } finally {
            lock.unlock(); // 解锁
        }
    }

    public String load03() {
        // redis使用setNX加锁，如果成功将返回1，失败就是0
//        Boolean lock = redis.opsForValue().setIfAbsent("lock", "111");
        // 设置锁10s之后过期
        String uuid = UUID.randomUUID().toString().substring(0, 4);
        Boolean lock = redis.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
        if (lock) {
            try {
                Object value = redis.opsForValue().get(CATEGORY_KEY);
                if (value != null) {
                    return JSON.toJSONString(value);
                }
                System.out.println("访问数据库。。。。。。。。。。。。。。。。。。。。。。" + Thread.currentThread().getName());
                String menus = listTree();
                try {
//                    TimeUnit.SECONDS.sleep(30);
                    TimeUnit.SECONDS.sleep(11);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                /**
                 * 在某种极端的情况下，在执行这行代码的时候，失败了。那么就会出现死锁的情况
                 */
                // 保存缓存中
                redis.opsForValue().set(CATEGORY_KEY, menus);
                return menus;
            } finally {
                // 解锁
//                Object o = redis.opsForValue().get("lock");
//                if (o.toString().equals(uuid)) {
//                    redis.delete("lock");
//                }
                // 解决解锁问题2，一下的操作是一个原子性的操作
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                redis.execute(new DefaultRedisScript(script), Arrays.asList("lock"), uuid);
            }
        } else {
            /**
             * 自旋等待
             * leccture底层使用的是netty进行通信，在自旋的时候，出现了异常。
             * 所以切换为jedis来连接redis
             */
            System.out.println("等待加锁");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return load();
        }
    }

    public String load02() {
        // synchronized进行锁的时候，必须时public修饰符
        synchronized (this) {
            Object value = redis.opsForValue().get(CATEGORY_KEY);
            if (value != null) {
                return JSON.toJSONString(value);
            }
            try {
                TimeUnit.SECONDS.sleep(20);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("访问数据库。。。。。。。。。。。。。。。。。。。。。。" + Thread.currentThread().getName());
            String menus = listTree();
            // 保存缓存中
            redis.opsForValue().set(CATEGORY_KEY, menus);
            return menus;
        }
    }

    private String load01() {
        System.out.println("访问数据库。。。。。。。。。。。。。。。。。。。。。。");
        String menus = listTree();
        // 模拟redis保存时，网络延时。。。
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 保存缓存中
        redis.opsForValue().set(CATEGORY_KEY, menus);
        return menus;
    }


    // 模式数据库数据
    private String listTree() {
        return "获取数据库中的菜单数据";
    }


}
